#
# This file is a part of the open-eBackup project.
# This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
# If a copy of the MPL was not distributed with this file, You can obtain one at
# http://mozilla.org/MPL/2.0/.
#
# Copyright (c) [2024] Huawei Technologies Co.,Ltd.
#
# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
# EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
# MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
#

import json
import os

from common.cleaner import clear
from common.common import output_execution_result, exter_attack
from common.const import ParamConstant, ExecuteResultEnum, AuthType
from common.enums.common_enums import DeployTypeEnum
from common.exception.common_exception import ErrCodeException
from common.logger import Logger
from common.parse_parafile import get_env_variable, add_env_param
from generaldb.saphana.comm.common_util import get_db_distributed_nodes_str, get_agent_ips_str, get_hostnames_in_env
from generaldb.saphana.comm.saphana_const import SaphanaModel, SaphanaErrorCode, SaphanaClusterStatus, \
    SaphanaJsonConstant, \
    SaphanaServiceConst, SaphanaConst
from generaldb.saphana.comm.saphana_resource_param import SaphanaResourceParam
from generaldb.saphana.resources.saphana_cluster_manager import SaphanaClusterManage

log = Logger().get_logger("saphana.log")

# 认证的敏感自定义参数模块
AUTH_CUSTOM_PARAMS = [SaphanaJsonConstant.SYSTEM_DB_USER, SaphanaJsonConstant.SYSTEM_DB_PASSWORD]
CUSTOM_PARAMS = [SaphanaJsonConstant.SYSTEM_ID, SaphanaJsonConstant.SYSTEM_DB_PORT]

# 租户数据库部署类型
TENANT_DB_DEPLOY_TYPE = [DeployTypeEnum.SINGLE.value, DeployTypeEnum.DISTRIBUTED.value]
# 系统数据库部署类型
SYSTEM_DB_DEPLOY_TYPE = [DeployTypeEnum.SINGLE.value, DeployTypeEnum.AP.value]


class SapHanaResourceService:
    def __init__(self, service_type: str, pid: str, context: dict):
        """
        init方法 针对SupportResource、CheckApplication。需要针对不同的接口去初始化参数。
        :param service_type: 接口业务类型
        :param pid: pid
        :param context: 入参
        """
        self._system_db_user = None
        self._system_id = None
        self._system_db_port = None
        self._pid = pid
        self._sensitive_custom_params = ""
        self._result_path = os.path.join(ParamConstant.RESULT_PATH, f"result{self._pid}")
        self._system_db_password = ""
        try:
            self._init_param(service_type, context)
        except ErrCodeException as ex:
            self._clean_self_param()
            log.exception(ex)
            log.error(f"Resource service init with pid {self._pid}.", ex)
            result_json = self._get_error_param_with_support_resource(
                int(ex.error_code), ex.error_message, ex.parameter_list)
            output_execution_result(self._result_path, result_json)
            raise ex

    @staticmethod
    def check_auth_type(auth_type):
        if int(auth_type) != AuthType.APP_PASSWORD.value:
            raise ErrCodeException(SaphanaErrorCode.NOT_SUPPORT_LOGIN_MODEL,
                                   message=f"auth type not support {auth_type}")

    @staticmethod
    def check_deploy_type(deploy_type: str, manager: SaphanaClusterManage):
        if not manager.is_system_db():
            allow_deploy_types = TENANT_DB_DEPLOY_TYPE
        else:
            allow_deploy_types = SYSTEM_DB_DEPLOY_TYPE
        if not deploy_type:
            raise ErrCodeException(SaphanaErrorCode.ERROR_PARAM, message="deploy type is null")
        if deploy_type not in allow_deploy_types:
            raise ErrCodeException(SaphanaErrorCode.DEPLOY_TYPE_ERROR, message=f"deploy type {deploy_type} is error")

    @staticmethod
    def _get_error_param_with_support_resource(code: int, message: str, code_params: list):
        return {"exception": {"code": code, "message": message,
                              "codeParams": code_params}}

    @staticmethod
    def _get_return_param_with_check_application(code: int, error_code: int, message: str):
        return {"code": code, "bodyErr": error_code, "message": message}

    @exter_attack
    def _init_param(self, service_type: str, context: dict):
        log.info(f"context: {context}")
        self._agent_ip_list = []
        self._endpoint_ip_list = []
        self._service_type = service_type
        self._protect_env = context.get(SaphanaJsonConstant.APP_ENV, {})
        if service_type == SaphanaServiceConst.LIST_APPLICATION_RESOURCE:
            # 注册和修改的场景，需要获取之前的资源信息，用于对比参数
            origin_protect_env_str = context.get(SaphanaJsonConstant.APP_ENV, {}) \
                .get(SaphanaJsonConstant.EXTEND_INFO, {}).get(SaphanaJsonConstant.ORIGIN_PROTECTED_RESOURCE)
            if origin_protect_env_str:
                self._origin_protect_env = json.loads(origin_protect_env_str)
            else:
                self._origin_protect_env = None
            # 注册和修改的场景，获取agent ip list
            self._init_agent_list()
            # 注册和修改的场景，判断认证模式是否是数据库认证
            SapHanaResourceService.check_auth_type(
                get_env_variable(f"{SaphanaJsonConstant.APP_ENV}_{SaphanaJsonConstant.AUTH}_"
                                 f"{SaphanaJsonConstant.AUTH_TYPE}_{self._pid}"))
        # 获取备份的数据库的用户和密码
        self._db_user = get_env_variable(f"{SaphanaJsonConstant.APP_ENV}_{SaphanaJsonConstant.AUTH}_"
                                         f"{SaphanaJsonConstant.AUTH_KEY}_{self._pid}")
        self._db_pwd_env_key = f"{SaphanaJsonConstant.APP_ENV}_{SaphanaJsonConstant.AUTH}_" \
                               f"{SaphanaJsonConstant.AUTH_PWD}_{self._pid}"
        # 获取自定义参数
        self._sensitive_custom_params = get_env_variable(f"{SaphanaJsonConstant.APP_ENV}_"
                                                         f"{SaphanaJsonConstant.AUTH}_"
                                                         f"{SaphanaJsonConstant.EXTEND_INFO}_"
                                                         f"{SaphanaJsonConstant.AUTH_CUSTOM_PARAMS}_{self._pid}")
        try:
            SaphanaResourceParam.check_custom_params(self._sensitive_custom_params, AUTH_CUSTOM_PARAMS)
        except ErrCodeException as ex:
            log.exception(ex)
            raise ErrCodeException(SaphanaErrorCode.ERROR_AUTH, "db auth param error") from ex
        protect_env_ext = self._protect_env.get(SaphanaJsonConstant.EXTEND_INFO, {})
        no_sensitive_custom_params = protect_env_ext.get(SaphanaJsonConstant.CUSTOM_PARAMS, "")
        SaphanaResourceParam.check_custom_params(no_sensitive_custom_params, CUSTOM_PARAMS)
        # 获取保护的数据库名称
        self._protect_db_name = self._protect_env.get(SaphanaJsonConstant.NAME)
        # 解析自定义参数
        self._init_custom_params(self._sensitive_custom_params, no_sensitive_custom_params)
        # check参数是否符合规定
        if not self._db_user:
            raise ErrCodeException(SaphanaErrorCode.ERROR_PARAM, message="db user is null")
        if not get_env_variable(self._db_pwd_env_key):
            raise ErrCodeException(SaphanaErrorCode.ERROR_PARAM, message="db pwd is null")
        if not self._system_db_port:
            raise ErrCodeException(SaphanaErrorCode.ERROR_PARAM, message="db port is null")
        if not self._system_id:
            raise ErrCodeException(SaphanaErrorCode.ERROR_PARAM, message="system id is null")
        if not self._protect_db_name:
            raise ErrCodeException(SaphanaErrorCode.ERROR_PARAM, message="protect db name is null")
        SaphanaResourceParam.check_param_is_contain_space([
            self._system_id, self._system_db_port,
            self._system_db_user, get_env_variable(self._system_db_password),
            self._protect_db_name,
            self._db_user, get_env_variable(self._db_pwd_env_key)])
        clear(self._sensitive_custom_params)

    def get_manager(self):
        """
        hana资源接入对外暴露的方法
        :return: 没有返回值，返回值直接写入到对应的结果文件
        """
        params = {
            SaphanaJsonConstant.SYSTEM_ID: self._system_id,
            SaphanaJsonConstant.SYSTEM_DB_USER: self._system_db_user,
            SaphanaJsonConstant.PROTECT_DATABASE: self._protect_db_name,
            SaphanaJsonConstant.SYSTEM_DB_PORT: self._system_db_port
        }
        try:
            return SaphanaClusterManage(params, self._pid)
        except ErrCodeException as ex:
            log.exception(ex)
            result_json = self._get_error_param_with_support_resource(
                int(ex.error_code), ex.error_message, ex.parameter_list)
            output_execution_result(self._result_path, result_json)
            raise ex

    def check_when_update(self, manager: SaphanaClusterManage):
        origin_protect_env_ext = self._origin_protect_env.get(SaphanaJsonConstant.EXTEND_INFO, {})
        origin_protect_env_uuid = origin_protect_env_ext.get(SaphanaJsonConstant.CLUSTER_UUID)
        protect_env_uuid = manager.get_uuid()
        if protect_env_uuid != origin_protect_env_uuid:
            raise ErrCodeException(SaphanaErrorCode.NOT_ALLOW_CHANGE_DB_TO_NEW_DB,
                                   message=f"origin db {origin_protect_env_uuid}, new db {protect_env_uuid}")
        log.info(f"Support resource cluster is not change. with pid {self._pid}")
        origin_protect_db_name = self._origin_protect_env.get(SaphanaJsonConstant.NAME)
        if self._protect_db_name != origin_protect_db_name:
            raise ErrCodeException(SaphanaErrorCode.NOT_ALLOW_CHANGE_PROTECT_DB,
                                   message=f"origin db {origin_protect_db_name}, "
                                           f"new db {self._protect_db_name}")
        log.info(f"Support resource protect db is not change with pid {self._pid}.")

    def list_application_resource(self, manager: SaphanaClusterManage):
        """
        list_application_resource接口
        :param manager hana集群管理类
        :return: 没有返回值，返回值直接写入到对应的结果文件
        """
        try:
            self._list_application_resource(manager)
        except ErrCodeException as ex:
            self._clean_self_param()
            log.exception(ex)
            log.error(f"Support resource errcode error with pid {self._pid}.", ex)
            result_json = self._get_error_param_with_support_resource(
                int(ex.error_code), ex.error_message, ex.parameter_list)
            output_execution_result(self._result_path, result_json)
            return
        self._clean_self_param()
        log.info(f"Support resource success with pid {self._pid}. protect env {self._protect_env}.")
        resource_list = [self._protect_env]
        return_json = {SaphanaJsonConstant.RESOURCE_LIST: resource_list}
        output_execution_result(self._result_path, return_json)

    def check_application(self, manager: SaphanaClusterManage):
        """
        check application接口
        :param manager hana集群管理类
        :return: 没有返回值，返回值直接写入到对应的结果文件
        """
        try:
            self._check_application_service(manager, True)
        except ErrCodeException as ex:
            self._clean_self_param()
            log.exception(ex)
            log.error(f"Check application errcode error with pid {self._pid}.", ex)
            result_json = self._get_return_param_with_check_application(
                int(ExecuteResultEnum.INTERNAL_ERROR), int(ex.error_code), ex.error_message)
            output_execution_result(self._result_path, result_json)
            return
        self._clean_self_param()
        log.info(f"Check application success with pid {self._pid}.")
        result_json = self._get_return_param_with_check_application(
            int(ExecuteResultEnum.SUCCESS), int(ExecuteResultEnum.SUCCESS), "check application success!")
        output_execution_result(self._result_path, result_json)

    def _init_agent_list(self):
        hosts = self._protect_env.get(SaphanaJsonConstant.NODES, {})
        for host_dict in hosts:
            host_ext = host_dict.get(SaphanaJsonConstant.EXTEND_INFO, {})
            agent_ip_list_str = host_ext.get(SaphanaJsonConstant.AGENT_IP_LIST)
            if not agent_ip_list_str:
                raise ErrCodeException(SaphanaErrorCode.ERROR_PARAM, message="agent ip is null")
            self._agent_ip_list.append(agent_ip_list_str)
            self._endpoint_ip_list.append(host_dict.get(SaphanaJsonConstant.ENDPOINT))
        if len(self._agent_ip_list) == 0:
            raise ErrCodeException(SaphanaErrorCode.ERROR_PARAM, message="agent ip list is null")

    def _init_custom_params(self, custom_params: str, sensitive_custom_params: str):
        custom_param_dict = SaphanaResourceParam.init_custom_params(
            custom_params + "," + sensitive_custom_params)
        if custom_param_dict.get(SaphanaJsonConstant.SYSTEM_DB_USER):
            self._system_db_user = custom_param_dict.get(SaphanaJsonConstant.SYSTEM_DB_USER)
        if custom_param_dict.get(SaphanaJsonConstant.SYSTEM_DB_PASSWORD):
            self._system_db_password = custom_param_dict.get(SaphanaJsonConstant.SYSTEM_DB_PASSWORD)
            # 将系统数据库密码放到环境变量中
            add_env_param(SaphanaJsonConstant.SYSTEM_DB_PASSWORD + self._pid, self._system_db_password)
        if custom_param_dict.get(SaphanaJsonConstant.SYSTEM_ID):
            system_id = custom_param_dict.get(SaphanaJsonConstant.SYSTEM_ID)
            self._system_id = system_id.lower()
            protect_env_ext = self._protect_env.get(SaphanaJsonConstant.EXTEND_INFO, {})
            no_sensitive_custom_params = protect_env_ext.get(SaphanaJsonConstant.CUSTOM_PARAMS, "")
            protect_env_ext[SaphanaJsonConstant.CUSTOM_PARAMS] = no_sensitive_custom_params.replace(
                system_id, self._system_id)
        if custom_param_dict.get(SaphanaJsonConstant.SYSTEM_DB_PORT):
            self._system_db_port = custom_param_dict.get(SaphanaJsonConstant.SYSTEM_DB_PORT)

    def _clean_self_param(self):
        clear(self._sensitive_custom_params)
        clear(self._system_db_password)

    def _list_application_resource(self, manager: SaphanaClusterManage):
        # 资源在线离线检查
        self._check_application_service(manager, False)
        log.info(f"Support resource check application success with pid {self._pid}.")

        # 如果是租户数据库只能是单机、分布式；如果是系统数据库只能是单机、主备
        deploy_type = self._protect_env.get(SaphanaJsonConstant.EXTEND_INFO, {}) \
            .get(SaphanaJsonConstant.DEPLOY_TYPE, None)
        SapHanaResourceService.check_deploy_type(deploy_type, manager)

        # 如果是单机部署模式，则agent只能有一个
        if deploy_type == DeployTypeEnum.SINGLE.value and len(self._agent_ip_list) != 1:
            raise ErrCodeException(SaphanaErrorCode.TYPE_AND_AGENTS_NOT_MATCH,
                                   message="agent is not one. but deploy type is single")
        if deploy_type != DeployTypeEnum.SINGLE.value and len(self._agent_ip_list) < 2:
            raise ErrCodeException(SaphanaErrorCode.TYPE_AND_AGENTS_NOT_MATCH,
                                   message=f"agent len is {len(self._agent_ip_list)}. but deploy type is not single")

        # 如果不是单机部署模式，则检验节点是否完全匹配
        if deploy_type != DeployTypeEnum.SINGLE.value:
            manager.check_agent_enough(self._agent_ip_list, self._db_user, self._db_pwd_env_key, self._endpoint_ip_list)
        else:
            # 如果是单机部署模式，则检验节点数量是否完全匹配，能否连接就由后面的数据库登录来检验了
            db_distributed_nodes = manager.get_db_distributed_nodes(self._db_user, self._db_pwd_env_key)
            if len(set(db_distributed_nodes)) != 1:
                raise ErrCodeException(SaphanaErrorCode.DB_NODES_NOT_FULL_MATCH,
                                       get_db_distributed_nodes_str(db_distributed_nodes, get_hostnames_in_env()),
                                       get_agent_ips_str(self._endpoint_ip_list),
                                       message=f"Instance nodes {self._endpoint_ip_list} "
                                               f"not full match {db_distributed_nodes}.")

        # 如果是修改场景：检查hana集群是否改变、检查保护的数据库是否改变
        # 如果不是修改场景：获取hana版本号和流水号并返回
        if self._origin_protect_env:
            self.check_when_update(manager)
            protect_env_ext = self._protect_env.get(SaphanaJsonConstant.EXTEND_INFO, {})
            protect_env_ext[SaphanaJsonConstant.ORIGIN_PROTECTED_RESOURCE] = ""
        else:
            version = manager.get_version()
            version_arr = version.split(".")
            if len(version_arr) > 1:
                version = version_arr[0] + "." + version_arr[1]
            self._protect_env[SaphanaJsonConstant.PATH] = self._protect_db_name
            protect_env_ext = self._protect_env.get(SaphanaJsonConstant.EXTEND_INFO, {})
            protect_env_ext[SaphanaJsonConstant.CLUSTER_UUID] = manager.get_uuid()
            protect_env_ext[SaphanaJsonConstant.APPLICATION_VERSION] = version.replace("00", "0")
            log.info(f"Support resource supply version and cluster uuid success with pid {self._pid}.")
        if manager.is_system_db():
            protect_env_ext[SaphanaJsonConstant.IS_NEED_CLOSE_DB] = "0"
        else:
            protect_env_ext[SaphanaJsonConstant.IS_NEED_CLOSE_DB] = "1"

    def _check_application_service(self, manager: SaphanaClusterManage, is_strict_for_system_db: bool):
        # 检验租户模式是否支持（目前只支持单租户、多租户），同时检验了系统id对应的adm用户是否存在
        model = manager.get_model()
        log.info(f"Cluster model is: {model}")
        if model and model != SaphanaModel.MULTI_TENANT_MODE:
            raise ErrCodeException(SaphanaErrorCode.TENANT_MODEL_NOT_SUPPORT,
                                   message=f"Tenant model {model} not support.")

        # 获取集群状态，以及当前要备份的数据库是不是系统数据库
        cluster_status = manager.get_cluster_status()
        is_system_db = manager.is_system_db()
        log.info(f"Register db is system db: {is_system_db}")

        # 如果是租户数据库，则系统数据库用户名和密码不能为空
        if not is_system_db:
            if not self._system_db_user or not self._system_db_password:
                raise ErrCodeException(SaphanaErrorCode.LACK_SYSTEM_INFO, message="system db param is null")

        # 如果是租户数据库，则集群必须在线
        if not is_system_db and cluster_status != SaphanaClusterStatus.ONLINE:
            raise ErrCodeException(SaphanaErrorCode.CLUSTER_NOT_ONLINE,
                                   message=f"Cluster status {cluster_status} is not online.")

        # 连通性检测和健康检查的场景，针对系统数据库，需要检验集群是否在线，但是注册和修改的场景不用强检验
        if is_strict_for_system_db and is_system_db and cluster_status != SaphanaClusterStatus.ONLINE:
            raise ErrCodeException(SaphanaErrorCode.CLUSTER_NOT_ONLINE,
                                   message=f"Cluster status {cluster_status} is not online.")

        disable_backint = """
alter system alter configuration ('global.ini', 'SYSTEM') unset ('backup', 'log_backup_using_backint') with reconfigure;
alter system alter configuration ('global.ini', 'SYSTEM') unset ('backup', 'catalog_backup_using_backint') with reconfigure;
"""
        # 如果集群在线，则检查用户名和密码是否能连接数据库
        if cluster_status == SaphanaClusterStatus.ONLINE:
            ret, out = manager.execute_query_in_db(self._protect_db_name, self._db_user, self._db_pwd_env_key)
            if not ret:
                raise ErrCodeException(SaphanaErrorCode.ACCESS_DB_ERROR,
                                    message=f"Login db {self._protect_db_name} by protect db user failed.")
            log.info(f"Check tenant db login success with {self._pid}.")

            # 如果是租户数据库，还需要检验系统数据库的用户和密码是否能连接数据库
            if not is_system_db:
                ret, out = manager.execute_query_in_db(SaphanaConst.SYSTEM_DB, self._system_db_user,
                                                    SaphanaJsonConstant.SYSTEM_DB_PASSWORD + self._pid, disable_backint)
                if not ret:
                    raise ErrCodeException(SaphanaErrorCode.LOGIN_SYSTEM_DB_ERROR,
                                        message=f"Login db {self._protect_db_name} by system db user failed.")
                log.info(f"Check system db login success with {self._pid}.")
            else:
                ret, out = manager.execute_query_in_db(self._protect_db_name, self._db_user, self._db_pwd_env_key,
                                                       disable_backint)
        else:
            log.warn(f"Cluster is not online.so not check login with {self._pid}.")

